module ActsAsProxy
  def self.included(base) #:nodoc:
    base.send(:extend, ClassMethods)
    base.after_save {|record| record.unsaved_proxied.all? {|associated| associated.save}}
  end

  # When the proxied object is not a new_record, ActiveRecord will not automatically take care of saving it.
  # So, ActsAsProxy stuffs them in here for a cascading save when the main record gets saved.
  def unsaved_proxied
    @unsaved_proxied ||= []
  end

  module ClassMethods
    # Creates proxies from one model to another associated model's attributes.
    #
    # Example setup:
    #
    #   class User < ActiveRecord::Base
    #     has_one :address
    #     has_many :aliases
    #
    #     proxies :address, :city, :state, :zip
    #     proxies :aliases, :name
    #   end
    #
    # This creates the following methods on your user objects:
    #
    # * address_city
    # * address_state
    # * address_zip
    # * alias_name
    #
    # Notice that it's "alias_name" and not "aliases_name". ActsAsProxy only proxies the *first* member of the associated set, so it uses singular syntax.
    def proxies(association_name, *methods)
      if [:has_one, :belongs_to].include? reflect_on_association(association_name).macro
        write_singular_proxies(association_name, *methods)
      else
        write_plural_proxies(association_name, *methods)
      end
    end

    def write_singular_proxies(association_name, *methods) #:nodoc:
      methods.each do |method|
        class_eval <<-END, __FILE__, __LINE__
          def #{association_name}_#{method}
            #{association_name}.#{method} if #{association_name}
          end

          def #{association_name}_#{method}=(val)
            if #{association_name}
              unsaved_proxied << #{association_name}
            else
              self.#{association_name} = #{reflect_on_association(association_name).klass}.new
            end
            #{association_name}.#{method} = val
          end
        END
      end
    end

    def write_plural_proxies(association_name, *methods) #:nodoc:
      methods.each do |method|
        class_eval <<-END, __FILE__, __LINE__
          def #{association_name.to_s.singularize}_#{method}
            #{association_name}.first.#{method} if #{association_name}.first
          end

          def #{association_name.to_s.singularize}_#{method}=(val)
            if #{association_name}.size == 0
              #{association_name} << #{reflect_on_association(association_name).klass}.new
            else
              unsaved_proxied << #{association_name}.first
            end
            #{association_name}.first.#{method} = val
          end
        END
      end
    end
  end
end